﻿#region License

// Another free, open solution by someone who gets it.
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace Patterns.Validation.Advanced
{
	/// <summary>
	/// Provides extensions that facilitate fluent validation.
	/// </summary>
	public static class Extensions
	{
		/// <summary>
		/// Invokes the specified action on an subject so that you can chain it with any of the ShouldThrow or ShouldNotThrow 
		/// overloads.
		/// </summary>
		public static Action Invoking<T>(this T subject, Action<T> action)
		{
			return () => action(subject);
		}

		/// <summary>
		/// Provides methods for asserting the execution time of a method or property.
		/// </summary>
		/// <param name="subject">The object that exposes the method or property.</param>
		/// <param name="action">A reference to the method or property to measure the execution time of.</param>
		/// <returns>
		/// Returns an object for asserting that the execution time matches certain conditions.
		/// </returns>
		public static MemberExecutionTimeValidator<T> ExecutionTimeOf<T>(this T subject, Expression<Action<T>> action)
		{
			return new MemberExecutionTimeValidator<T>(subject, action);
		}

		/// <summary>
		/// Provides methods for asserting the execution time of a method or property.
		/// </summary>
		/// <param name="action">A reference to the method or property to measure the execution time of.</param>
		/// <returns>
		/// Returns an object for asserting that the execution time matches certain conditions.
		/// </returns>
		public static ExecutionTimeValidator ExecutionTime(this Action action)
		{
			return new ExecutionTimeValidator(action);
		}

		/// <summary>
		/// Validates that the <paramref name="action"/> throws an exception.
		/// </summary>
		/// <typeparam name="TException">
		/// The type of the exception it should throw.
		/// </typeparam>
		/// <returns>
		/// Returns an object that allows asserting additional members of the thrown exception.
		/// </returns>
		public static ExceptionValidator<TException> ShouldThrow<TException>(this Action action)
			 where TException : Exception
		{
			return ShouldThrow<TException>(action, String.Empty);
		}

		/// <summary>
		/// Validates that the <paramref name="action"/> throws an exception.
		/// </summary>
		/// <param name="action">A reference to the method or property.</param>
		/// <typeparam name="TException">
		/// The type of the exception it should throw.
		/// </typeparam>
		/// <param name="reason">
		/// A formatted phrase explaining why the assertion should be satisfied. If the phrase does not 
		/// start with the word <i>because</i>, it is prepended to the message.
		/// </param>
		/// <param name="reasonArgs">
		/// Zero or more values to use for filling in any <see cref="string.Format(string,object[])"/> compatible placeholders.
		/// </param>
		/// <returns>
		/// Returns an object that allows asserting additional members of the thrown exception.
		/// </returns>
		public static ExceptionValidator<TException> ShouldThrow<TException>(this Action action, string reason, params object[] reasonArgs)
			 where TException : Exception
		{
			return new ActionValidator(action).ShouldThrow<TException>(reason, reasonArgs);
		}

		/// <summary>
		/// Validates that the <paramref name="action"/> does not throw a particular exception.
		/// </summary>
		/// <typeparam name="TException">
		/// The type of the exception it should not throw. Any other exceptions are ignored and will satisfy the assertion.
		/// </typeparam>
		public static void ShouldNotThrow<TException>(this Action action)
		{
			ShouldNotThrow<TException>(action, String.Empty);
		}

		/// <summary>
		/// Validates that the <paramref name="action"/> does not throw a particular exception.
		/// </summary>
		/// <param name="action">The current method or property.</param>
		/// <typeparam name="TException">
		/// The type of the exception it should not throw. Any other exceptions are ignored and will satisfy the assertion.
		/// </typeparam>
		/// <param name="reason">
		/// A formatted phrase explaining why the assertion should be satisfied. If the phrase does not 
		/// start with the word <i>because</i>, it is prepended to the message.
		/// </param>
		/// <param name="reasonArgs">
		/// Zero or more values to use for filling in any <see cref="string.Format(string,object[])"/> compatible placeholders.
		/// </param>
		public static void ShouldNotThrow<TException>(this Action action, string reason, params object[] reasonArgs)
		{
			new ActionValidator(action).ShouldNotThrow<TException>(reason, reasonArgs);
		}

		/// <summary>
		/// Validates that the <paramref name="action"/> does not throw any exception at all.
		/// </summary>
		public static void ShouldNotThrow(this Action action)
		{
			ShouldNotThrow(action, String.Empty);
		}

		/// <summary>
		/// Validates that the <paramref name="action"/> does not throw any exception at all.
		/// </summary>
		/// <param name="action">The current method or property.</param>
		/// <param name="reason">
		/// A formatted phrase explaining why the assertion should be satisfied. If the phrase does not 
		/// start with the word <i>because</i>, it is prepended to the message.
		/// </param>
		/// <param name="reasonArgs">
		/// Zero or more values to use for filling in any <see cref="string.Format(string,object[])"/> compatible placeholders.
		/// </param>
		public static void ShouldNotThrow(this Action action, string reason, params object[] reasonArgs)
		{
			new ActionValidator(action).ShouldNotThrow(reason, reasonArgs);
		}

		/// <summary>
		/// Forces enumerating a collection. Should be used to assert that a method that uses the 
		/// <c>yield</c> keyword throws a particular exception.
		/// </summary>
		public static Action Enumerating(this Func<IEnumerable> enumerable)
		{
			return () => ForceEnumeration(enumerable);
		}

		/// <summary>
		/// Forces enumerating a collection. Should be used to assert that a method that uses the 
		/// <c>yield</c> keyword throws a particular exception.
		/// </summary>
		public static Action Enumerating<T>(this Func<IEnumerable<T>> enumerable)
		{
			return () => ForceEnumeration(() => (IEnumerable)enumerable());
		}

		private static void ForceEnumeration(Func<IEnumerable> enumerable)
		{
			foreach (var item in enumerable())
			{
				// Do nothing
			}
		}

		/// <summary>
		/// Returns an <see cref="ObjectValidator"/> object that can be used to assert the
		/// current <see cref="object"/>.
		/// </summary>
		public static ObjectValidator Should(this object actualValue)
		{
			return new ObjectValidator(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="BooleanValidator"/> object that can be used to assert the
		/// current <see cref="bool"/>.
		/// </summary>
		public static BooleanValidator Should(this bool actualValue)
		{
			return new BooleanValidator(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="NullableBooleanValidator"/> object that can be used to assert the
		/// current nullable <see cref="bool"/>.
		/// </summary>
		public static NullableBooleanValidator Should(this bool? actualValue)
		{
			return new NullableBooleanValidator(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="GuidValidator"/> object that can be used to assert the
		/// current <see cref="Guid"/>.
		/// </summary>
		public static GuidValidator Should(this Guid actualValue)
		{
			return new GuidValidator(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="NullableGuidValidator"/> object that can be used to assert the
		/// current nullable <see cref="Guid"/>.
		/// </summary>
		public static NullableGuidValidator Should(this Guid? actualValue)
		{
			return new NullableGuidValidator(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="NonGenericCollectionValidator"/> object that can be used to assert the
		/// current <see cref="IEnumerable"/>.
		/// </summary>
		public static NonGenericCollectionValidator Should(this IEnumerable actualValue)
		{
			return new NonGenericCollectionValidator(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="GenericCollectionValidator{T}"/> object that can be used to assert the
		/// current <see cref="IEnumerable{T}"/>.
		/// </summary>
		public static GenericCollectionValidator<T> Should<T>(this IEnumerable<T> actualValue)
		{
			return new GenericCollectionValidator<T>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="GenericDictionaryValidator{TKey, TValue}"/> object that can be used to assert the
		/// current <see cref="IDictionary{TKey, TValue}"/>.
		/// </summary>
		public static GenericDictionaryValidator<TKey, TValue> Should<TKey, TValue>(this IDictionary<TKey, TValue> actualValue)
		{
			return new GenericDictionaryValidator<TKey, TValue>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="DateTimeValidator"/> object that can be used to assert the
		/// current <see cref="DateTime"/>.
		/// </summary>
		public static DateTimeValidator Should(this DateTime actualValue)
		{
			return new DateTimeValidator(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="NullableDateTimeValidator"/> object that can be used to assert the
		/// current nullable <see cref="DateTime"/>.
		/// </summary>
		public static NullableDateTimeValidator Should(this DateTime? actualValue)
		{
			return new NullableDateTimeValidator(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="ComparableTypeValidator{T}"/> object that can be used to assert the
		/// current <see cref="IComparable{T}"/>.
		/// </summary>
		public static ComparableTypeValidator<T> Should<T>(this IComparable<T> comparableValue)
		{
			return new ComparableTypeValidator<T>(comparableValue);
		}

		/// <summary>
		/// Returns an <see cref="IntegralValidator{T}"/> object that can be used to assert the
		/// current <see cref="int"/>.
		/// </summary>
		public static IntegralValidator<int> Should(this int actualValue)
		{
			return new IntegralValidator<int>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="IntegralValidator{T}"/> object that can be used to assert the
		/// current nullable <see cref="int"/>.
		/// </summary>
		public static IntegralValidator<int?> Should(this int? actualValue)
		{
			return new IntegralValidator<int?>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="IntegralValidator{T}"/> object that can be used to assert the
		/// current <see cref="byte"/>.
		/// </summary>
		public static IntegralValidator<byte> Should(this byte actualValue)
		{
			return new IntegralValidator<byte>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="IntegralValidator{T}"/> object that can be used to assert the
		/// current nullable <see cref="byte"/>.
		/// </summary>
		public static IntegralValidator<byte?> Should(this byte? actualValue)
		{
			return new IntegralValidator<byte?>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="IntegralValidator{T}"/> object that can be used to assert the
		/// current <see cref="short"/>.
		/// </summary>
		public static IntegralValidator<short> Should(this short actualValue)
		{
			return new IntegralValidator<short>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="IntegralValidator{T}"/> object that can be used to assert the
		/// current nullable <see cref="short"/>.
		/// </summary>
		public static IntegralValidator<short?> Should(this short? actualValue)
		{
			return new IntegralValidator<short?>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="IntegralValidator{T}"/> object that can be used to assert the
		/// current <see cref="long"/>.
		/// </summary>
		public static IntegralValidator<long> Should(this long actualValue)
		{
			return new IntegralValidator<long>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="IntegralValidator{T}"/> object that can be used to assert the
		/// current nullable <see cref="long"/>.
		/// </summary>
		public static IntegralValidator<long?> Should(this long? actualValue)
		{
			return new IntegralValidator<long?>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="NumericValidator{T}"/> object that can be used to assert the
		/// current <see cref="float"/>.
		/// </summary>
		public static NumericValidator<float> Should(this float actualValue)
		{
			return new NumericValidator<float>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="NumericValidator{T}"/> object that can be used to assert the
		/// current nullable <see cref="float"/>.
		/// </summary>
		public static NumericValidator<float?> Should(this float? actualValue)
		{
			return new NumericValidator<float?>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="NumericValidator{T}"/> object that can be used to assert the
		/// current <see cref="double"/>.
		/// </summary>
		public static NumericValidator<double> Should(this double actualValue)
		{
			return new NumericValidator<double>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="NumericValidator{T}"/> object that can be used to assert the
		/// current nullable <see cref="double"/>.
		/// </summary>
		public static NumericValidator<double?> Should(this double? actualValue)
		{
			return new NumericValidator<double?>(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="StringValidator"/> object that can be used to assert the
		/// current <see cref="string"/>.
		/// </summary>
		public static StringValidator Should(this string actualValue)
		{
			return new StringValidator(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="SimpleTimeSpanValidator"/> object that can be used to assert the
		/// current <see cref="TimeSpan"/>.
		/// </summary>
		public static SimpleTimeSpanValidator Should(this TimeSpan actualValue)
		{
			return new SimpleTimeSpanValidator(actualValue);
		}

		/// <summary>
		/// Returns an <see cref="NullableSimpleTimeSpanValidator"/> object that can be used to assert the
		/// current nullable <see cref="TimeSpan"/>.
		/// </summary>
		public static NullableSimpleTimeSpanValidator Should(this TimeSpan? actualValue)
		{
			return new NullableSimpleTimeSpanValidator(actualValue);
		}

		/// <summary>
		/// Validates that the properties of an object matches those of another object.
		/// </summary>
		public static PropertyValidator<T> ShouldHave<T>(this T subject)
		{
			return new PropertyValidator<T>(subject);
		}

		/// <summary>
		/// Returns a <see cref="TypeValidator"/> object that can be used to assert the
		/// current <see cref="Type"/>.
		/// </summary>
		public static TypeValidator Should(this Type subject)
		{
			return new TypeValidator(subject);
		}

		/// <summary>
		/// Returns a <see cref="MethodInfoValidator"/> object that can be used to assert the methods returned by the
		/// current <see cref="MethodInfoSelector"/>.
		/// </summary>
		/// <seealso cref="TypeValidator"/>
		public static MethodInfoValidator Should(this MethodInfoSelector methodSelector)
		{
			return new MethodInfoValidator(methodSelector.ToArray());
		}

		/// <summary>
		/// Returns a <see cref="PropertyInfoValidator"/> object that can be used to assert the properties returned by the
		/// current <see cref="PropertyInfoSelector"/>.
		/// </summary>
		/// <seealso cref="TypeValidator"/>
		public static PropertyInfoValidator Should(this PropertyInfoSelector propertyInfoSelector)
		{
			return new PropertyInfoValidator(propertyInfoSelector.ToArray());
		}

		/// <summary>
		/// Safely casts the specified object to the type specified through <typeparamref name="TTo"/>.
		/// </summary>
		/// <remarks>
		/// Has been introduced to allow casting objects without breaking the fluent API.
		/// </remarks>
		/// <typeparam name="TTo"></typeparam>
		public static TTo As<TTo>(this object subject)
		{
			return subject is TTo ? (TTo)subject : default(TTo);
		}
	}
}